﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LastoRevival.Service.Contracts;
using LastoRevival.Service.Implementations.Exceptions;
using LastoRevival.Service.Contracts.Models;
using LastoRevival.Service.Implementations.Data;
using mdryden.Core.Data;
using mdryden.Core.Security;
using LastoRevival.Service.Implementations.Models;
using LastoRevival.Service.Implementations.Core;
using LastoRevival.Service.Implementations.Settings;
using System.Web;

namespace LastoRevival.Service.Implementations
{
	public class UserService : ServiceBase, IUserService
	{

		private SimpleHash hashHandler;

		public UserService(SimpleHash hashHandler)
		{
			this.hashHandler = hashHandler;
		}

		private IDataProvider DataProvider
		{
			get
			{
				return DependencyFactory.GetDataProvider();
			}
		}


		public LoginAttemptResult Login(Guid loginToken, string username, string password, bool rememberMe)
		{
			UpdateSession(loginToken);

			// confirm that this user is verified
			var user = DataProvider.GetProfile(username);

			if (!user.Verified)
			{
				return new LoginAttemptResult(LoginResult.NotVerified);
			}

			var passwordHash = DataProvider.GetPasswordHash(username);

			var result = hashHandler.VerifyHash(password, passwordHash) ? LoginResult.Success : LoginResult.PasswordError;

			LoginAttemptResult response;

			if (result == LoginResult.Success)
			{
				// log the user in with this token
				var expirationDate = rememberMe ?
						DateTime.UtcNow.AddDays(ServiceSettings.GetInstance().RememberMeDays) :
						DateTime.UtcNow.AddMinutes(ServiceSettings.GetInstance().LoginMinutes);

				var token = DataProvider.StartUserSession(username, expirationDate);

				// add the token to the response
				response = new LoginAttemptResult(token, expirationDate);
			}
			else
			{
				// failed
				response = new LoginAttemptResult(result);
			}

			return response;

		}

		public void Logout(Guid loginToken)
		{
			DataProvider.EndUserSession(loginToken);
		}

		public bool IsAuthenticated(Guid loginToken)
		{
			return DataProvider.GetSessionExpiration(loginToken) > DateTime.UtcNow;
		}

		public ViewUserModel GetPublicProfile(Guid loginToken, string username)
		{
			VerifyLoggedIn(loginToken);
			UpdateSession(loginToken);

			var currentUser = DataProvider.GetProfile(loginToken);

			var details = DataProvider.GetProfile(username);
			var stats = DataProvider.GetUserStats(details.UserID);

			var canEdit = CanEdit(currentUser, details.UserID);
			var canDelete = CanDelete(currentUser);

			return new ViewUserModel { Details = details, Stats = stats, CanEdit = canEdit, CanDelete = canDelete };
		}

		public UserProfile GetActiveProfile(Guid loginToken)
		{
			UpdateSession(loginToken);

			var details = GetActiveProfileDetails(loginToken);
			var forumAccess = DataProvider.GetForumAccessForGroup(details.GroupID);

			return UserProfile.Create(details, forumAccess);
			
		}

		private UserProfileDetails GetActiveProfileDetails(Guid loginToken)
		{
			try
			{
				return DataProvider.GetProfile(loginToken);
			}
			catch (InvalidRowCountException)
			{
				return UserProfileDetails.CreateAnonymous();
			}
		}


		public CreateUserAttemptResult CreateUser(Guid loginToken, CreateUserModel user)
		{
			UpdateSession(loginToken);

			// doesn't require login, since this is the register function
			// don't do anything if the username is taken
			if (!DataProvider.IsUsernameAvailable(user.UserName))
			{
				return new CreateUserAttemptResult(CreateUserResult.DuplicateUsername);
			}

			// hash the user's password
			// weird little gotcha here - if this method is called locally the user is passed as a reference type
			// and thus just setting the user's password to the hashed password will change the original reference
			// that caused a nice little bug where I couldn't log the user in after creating them.
			// to avoid that, I'll create a new user which is a clone of the one passed in.
			var userToCreate = user.Clone();
			userToCreate.Password = hashHandler.ComputeHash(user.Password);

			var code = VerificationCode.Create();
			var groupID = (int)BuiltInGroups.Registered;
									
			var userID = DataProvider.CreateUser(userToCreate, code, groupID);

			// if verification type is none, mark this user as verified
			if (ServiceSettings.GetInstance().UserVerificationType == UserVerificationType.None)
			{
				DataProvider.VerifyUser(user.UserName);
			}

			return new CreateUserAttemptResult(userID, ServiceSettings.GetInstance().UserVerificationType);
		}

		public EditProfileModel GetProfileForEdit(Guid loginToken, string username)
		{
			VerifyLoggedIn(loginToken);
			UpdateSession(loginToken);

			// get the current user so we can verify access
			var profile = DataProvider.GetProfile(loginToken);
			var user = DataProvider.GetProfileForEditing(username);

			VerifyEditPermissions(profile, user.UserID);

			return user;
		}

		public void UpdateProfile(Guid loginToken, EditProfileModel user)
		{
			VerifyLoggedIn(loginToken);
			UpdateSession(loginToken);

			var profile = DataProvider.GetProfile(loginToken);

			VerifyEditPermissions(profile, user.UserID);

			DataProvider.UpdateProfile(user);
		}

		public string GetUsername(Guid loginToken, int userID)
		{
			VerifyLoggedIn(loginToken);
			UpdateSession(loginToken);

			return DataProvider.GetUsername(userID);
		}

		private void VerifyEditPermissions(UserProfileDetails currentUser, int userID)
		{
			if (!CanEdit(currentUser, userID))
			{
				throw new NoAccessException();
			}
		}

		private bool CanEdit(UserProfileDetails currentUser, int userID)
		{
			// must either be an admin, or the user himself
			if (currentUser.UserID == userID)
			{
				return true;
			}

			if (currentUser.IsAdministrator)
			{

				return true;
			}

			// nope
			return false;
		}

		private bool CanDelete(UserProfileDetails currentUser)
		{
			// must be an admin
			return currentUser.IsAdministrator;
		}

		public bool VerifyUser(Guid loginToken, VerifyUserModel user)
		{
			UpdateSession(loginToken);

			var code = DataProvider.GetUserVerificationCode(user.Username);

			// deleting users with expired codes could accidentally delete an authorized user.  don't let that happen!
			var profile = DataProvider.GetProfile(user.Username);

			if (profile.Verified)
			{
				throw new UserIsVerifiedException();
			}

			// check for an expired code, users with expired codes get deleted
			if (code.Expiration < DateTime.UtcNow)
			{
				// expired
				DataProvider.DeleteUser(user.Username);
				return false;
			}

			if (code.Code == user.Code)
			{
				DataProvider.VerifyUser(user.Username);
				return true;
			}
			else
			{
				return false;
			}
		}

		public void SendVerificationEmail(Guid loginToken, string username, string messageBodyFormat)
		{

			UpdateSession(loginToken);

			var profile = DataProvider.GetProfile(username);

			if (profile.Verified)
			{
				// already verified
				throw new UserIsVerifiedException();
			}
			
			// confirm code is still valid
			var code = DataProvider.GetUserVerificationCode(username);

			if (code.Expiration < DateTime.UtcNow)
			{
				// expired.  delete user and fail
				DataProvider.DeleteUser(username);
				throw new InvalidRowCountException(1, 0); // no users
			}

			// resend email.  does not change the original expiration
			var user = DataProvider.GetProfileForEditing(username);

			var encodedCode = HttpUtility.UrlEncode(code.Code);
			var body = string.Format(messageBodyFormat, encodedCode);
			
			var emailer = Emailer.Create();
			emailer.SendEmail(user.Email, ServiceSettings.GetInstance().FromEmail, "Verification Code", body);
			

		}

		public IEnumerable<OnlineUser> GetOnlineUsers(Guid loginToken)
		{
			UpdateSession(loginToken);

			var threshold = DateTime.Now.AddMinutes(ServiceSettings.GetInstance().ActiveSessionMinutes * -1); // anyone in last 10 minutes is considered online

			return DataProvider.GetOnlineUsers(threshold);
		}
	}
}
